/**
 * @file myfind.c
 * Main module for myfind-program
 * Beispiel 1
 *
 * @author Christian Lachner <ic11b010@technikum-wien.at>
 * @author Philip Weiskopf <ic11b085@technikum-wien.at>
 * @author Max Paflik <ic11b006@technikum-wien.at>
 * @date 2012/03/12
 *
 * @version $Revision: 60 $
 *
 * URL: $HeadURL$
 *
 * Last Modified: $Author: Chris $
 */

 /*
 * -------------------------------------------------------------- includes --
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <fnmatch.h>
#include "myfind.h"

/**
 *
 * \brief A simplified implementation of the GNU find-util.
 *
 * This is the main entry point for the program.
 * The program will find files and folders of a specified directory and 
 * output the results according to the given arguments.
 * 
 * The following arguments are allowed:
 * 
 * -h --help 			will output usage instructions of the program.
 * -user <name>|<uid> 	will find files, matching username or userid.
 * -nouser 				will find files, with no valid userid.
 * -name <pattern> 		will find files, matching the pattern.
 * -path <pattern> 		will find files, matching the pattern.
 * -type [bcdpfls] 		will find files, matching the filetype.
 * -print 				will output the results in print format.
 * -ls 					will putput the results in ls format.
 *
 * \param argc the number of arguments
 * \param argv the arguments itselves (including the program name in argv[0])
 *
 * \return int
 * \retval 0 If all went right.
 * \retval 1 If something wrong
 *
 */
int main(int argc, char * argv[]) {
	int argvc = 1;	/* counter for argument handler loop */

	if(argc <= 1) {
		printusage(argv);
		exit(EXIT_SUCCESS);
	} else {
		while(argvc < argc) {
			if((strcmp(argv[argvc], "--help") == 0) || (strcmp(argv[argvc], "-h") == 0)) {
				printusage(argv);
				exit(EXIT_SUCCESS);
			}
			argvc++;
		}
	}

	if(do_file(argv[1], (const char * const *) argv) == 1) {
		exit(EXIT_SUCCESS);
	} else {
		exit(EXIT_FAILURE);
	}
}

/**
 *
 * \brief Function to iterate through dirs and call do_file.
 *
 * This function is designed to be called by do_file(). It is used
 * to iterate through dir_name's content by achieving stat-information
 * using lstat and call do_file with each dir-entry.
 * 
 * \param dir_name Name of an directory.
 * \param parms Parameters of the argument vector.
 * 
 * \return int
 * \retval 0 error.
 * \retval 1 all ok.
 *
 */
int do_dir(const char * dir_name, const char * const * parms) {
	char * full_file_name = NULL;
	struct stat stats;
	DIR * dirstream = NULL;
	struct dirent *dirent = NULL;

	if(lstat(dir_name, &stats) == -1) {
		fprintf(stderr, "Error while getting status of \"%s\": ", dir_name);

		if(errno == EACCES) fprintf(stderr, "Search permission is denied\n");
		else if(errno == EBADF) fprintf(stderr, "Bad filedescriptor\n");
		else if(errno == EFAULT) fprintf(stderr, "Bad address\n");
		else if(errno == ELOOP) fprintf(stderr, "Too many symbolic links encountered while traversing the path.\n");
		else if(errno == ENAMETOOLONG) fprintf(stderr, "Path is too long.\n");
		else if(errno == ENOENT) fprintf(stderr, "A component of path does not exist, or path is an empty string.\n");
		else if(errno == ENOMEM) fprintf(stderr, "Out of memory\n");
		else if(errno == ENOTDIR) fprintf(stderr, "A component of the path prefix of path is not a directory.\n");
		else if(errno == EOVERFLOW) fprintf(stderr, "File size cannot be represented in the type off_t\n");
		else fprintf(stderr, "Unknown Error: %i.\n", errno);

		return 0;
	}

	if((dirstream = opendir(dir_name)) == NULL) {
		fprintf(stderr, "Error while opening \"%s\": ", dir_name);

		if(errno == EACCES) fprintf(stderr, "Permission denied\n");
		else if(errno == EBADF) fprintf(stderr, "FD is not a valid file descriptor opened for reading.\n");
		else if(errno == EMFILE) fprintf(stderr, "Too many file descriptors in use by process.\n");
		else if(errno == ENFILE) fprintf(stderr, "Too many files are currently open in the system.");
		else if(errno == ENOENT) fprintf(stderr, "Directory does not exist.\n");
		else if(errno == ENOMEM) fprintf(stderr, "Insufficient memory to complete the operation.\n");
		else if(errno == ENOTDIR) fprintf(stderr, "Not a directory.\n");
		else fprintf(stderr, "Unknown Error: %i.\n", errno);

		return 0;
	}

	while((dirent = readdir(dirstream)) != NULL) {
		if((strcmp(dirent->d_name, ".") != 0) &&
			(strcmp(dirent->d_name, "..") != 0)) {
				full_file_name = malloc(strlen(dir_name) + strlen(dirent->d_name) + 2);
				if(full_file_name == NULL) {
					fatal_error(parms, "Failed to allocate memory.\n", 1);
				}
				strcpy(full_file_name, strip_slashes((char *) dir_name));

				if(strcmp(dir_name, "/") != 0) {
					strncat(full_file_name, "/", 1);
				}
				strncat(full_file_name, dirent->d_name, strlen(dirent->d_name));

				do_file(full_file_name, parms);

				free(full_file_name);
		}
	}

	if(closedir(dirstream) != 0) {
		if(errno == EBADF) fprintf(stderr, "Invalid directory stream descriptor.\n");
		else fprintf(stderr, "Unknown Error: %i.\n", errno);

		return 0;
	}

	return 1;
}

/**
 *
 * \brief Gets lstat from a file or directory and calls parameter handler.
 *
 * This function is designed to achieve stat-information of the given file/dir
 * from a file or directory using lstat and calls the parameter afterwards.
 * 
 * \param full_file_name Full filename including path.
 * \param parms Parameters of the argument vector.
 *
 * \return int
 * \retval 0 error.
 * \retval 1 all ok.
 *
 */
int do_file(const char * full_file_name, const char * const * parms) {
	struct stat stats;

	if(lstat(full_file_name, &stats) == -1) {
		fprintf(stderr, "Error while getting status of \"%s\": ", full_file_name);

		if(errno == EACCES) fprintf(stderr, "Search permission is denied\n");
		else if(errno == EBADF) fprintf(stderr, "Bad filedescriptor\n");
		else if(errno == EFAULT) fprintf(stderr, "Bad address\n");
		else if(errno == ELOOP) fprintf(stderr, "Too many symbolic links encountered while traversing the path.\n");
		else if(errno == ENAMETOOLONG) fprintf(stderr, "Path is too long.\n");
		else if(errno == ENOENT) fprintf(stderr, "A component of path does not exist, or path is an empty string.\n");
		else if(errno == ENOMEM) fprintf(stderr, "Out of memory\n");
		else if(errno == ENOTDIR) fprintf(stderr, "A component of the path prefix of path is not a directory.\n");
		else if(errno == EOVERFLOW) fprintf(stderr, "File size cannot be represented in the type off_t\n");
		else fprintf(stderr, "Unknown Error: %i.\n", errno);

		return 0;
	}

	handle_parms(full_file_name, parms, stats);

	if(S_ISDIR(stats.st_mode)) {
		do_dir(full_file_name, parms);
	}

	return 1;
}

/**
 *
 * \brief Gets called by do_file and handles argv-parameters for
 * full_file_name
 *
 * Gets usually called by do_file and handles argv-parameters for
 * full_file_name.
 * 
 * \param full_file_name Full filename including path.
 * \param parms Parameters of the argument vector.
 * \param stats Struct from lstat of full_file_name.
 * 
 * \return void
 *
 */
void handle_parms(const char * full_file_name, const char * const * parms, struct stat stats) {
	char * file_name = NULL;
	char * dir_name = NULL;
	int argvc = 2;	/* counter for argument handler loop */
	int parmc = 1;
	int prntflg = 0;
	int breakearlyflg = 0;
	char * tmp = malloc(MAX_PWNAME_SIZE);	 /* buffer for getentryowner in name-handler */
	while(parms[parmc] != NULL)
		parmc++;


	while(argvc < parmc) {
		if(strcmp(parms[argvc], "-name") == 0) {
			if(argvc < parmc-1) {
				argvc++;	/* option -name needs one argument */

				/* extract file-name and dir-name from full_file_name */
				if(file_name == NULL) {
					dir_name = malloc(strlen(full_file_name) + 1);
					strcpy(dir_name, full_file_name);

					file_name = strrchr(dir_name, '/');
					if(file_name != NULL) {
						file_name[0] = 0;
						file_name++;
					} else {
						file_name = dir_name;
						dir_name = NULL;
					}
				}

				if(fnmatch(parms[argvc], file_name, 0) != 0) {
					breakearlyflg = 1;
					break;
				}
			} else {
				free(tmp);
				fatal_error(parms, "option \"-name\" expects one more argument\n", 1);
			}
		} else if(strcmp(parms[argvc], "-user") == 0) {
			if(argvc < parmc-1) {
				argvc++;	/* option -user needs one argument */

				if(user_exists(parms[argvc])) {
					switch(getentryowner(stats.st_uid, &tmp, MAX_PWNAME_SIZE)) {
						case ENTRY_ERROR:
						case ID_NOT_FOUND:	breakearlyflg = 1;
											break;
						case ID_FOUND:
											if(strcmp(parms[argvc], tmp) != 0) {
												breakearlyflg = 1;
												break;
											}
						default:			break;
					}
					if(breakearlyflg == 1)
						break;
				} else {
					if(is_numeric((char *) parms[argvc])) {
						if(strtol(parms[argvc], NULL, 0) != (long) stats.st_uid) {
							breakearlyflg = 1;
							break;
						}
					} else {
						fatal_error(parms, "invalid argument for -user given\n", 1);
					}
				}
			} else {
				free(tmp);
				fatal_error(parms, "option \"-user\" expects one more argument\n", 1);
			}
		} else if(strcmp(parms[argvc], "-group") == 0) {
			if(argvc < parmc-1) {
				argvc++;	/* option -user needs one argument */

				if(group_exists(parms[argvc])) {
					switch(getentrygroup(stats.st_gid, &tmp, MAX_GRNAME_SIZE)) {
						case ENTRY_ERROR:
						case ID_NOT_FOUND:	breakearlyflg = 1;
											break;
						case ID_FOUND:
											if(strcmp(parms[argvc], tmp) != 0) {
												breakearlyflg = 1;
												break;
											}
						default:			break;
					}
					if(breakearlyflg == 1)
						break;
				} else {
					if(is_numeric((char *) parms[argvc])) {
						if(strtol(parms[argvc], NULL, 0) != (long) stats.st_uid) {
							breakearlyflg = 1;
							break;
						}
					} else {
						fatal_error(parms, "invalid argument for -group given\n", 1);
					}
				}
			} else {
				free(tmp);
				fatal_error(parms, "option \"-group\" expects one more argument\n", 1);
			}
		} else if(strcmp(parms[argvc], "-nouser") == 0) {
			if(getentryowner(stats.st_uid, &tmp, MAX_PWNAME_SIZE) != ID_NOT_FOUND) {
				breakearlyflg = 1;
				break;
			}
		} else if(strcmp(parms[argvc], "-nogroup") == 0) {
			if(getentrygroup(stats.st_gid, &tmp, MAX_GRNAME_SIZE) != ID_NOT_FOUND) {
				breakearlyflg = 1;
				break;
			}
		} else if(strcmp(parms[argvc], "-path") == 0) {
			if(argvc < parmc-1) {
				argvc++;	/* option -path needs one argument */
				if(fnmatch(parms[argvc], full_file_name, 0) != 0) {
					breakearlyflg = 1;
					break;
				}
			} else {
				free(tmp);
				fatal_error(parms, "option \"-path\" expects one more argument\n", 1);
			}
		} else if(strcmp(parms[argvc], "-type") == 0) {
			if(argvc < parmc-1) {
				argvc++;	/* option -type needs one argument */
				if(strlen(parms[argvc]) == 1) {
					switch(tolower(*parms[argvc])) {
						case 'f':	if('-' != getentrytype(&stats)) {
										breakearlyflg = 1;
									}
									break;
						case 'd':
						case 'c':
						case 'b':
						case 'p':
						case 'l':
						case 's':	if(tolower(*parms[argvc]) != getentrytype(&stats)) {
										breakearlyflg = 1;
									}
									break;

						default:	free(tmp);
									fatal_error(parms, "Wrong argument for \"-type\" given!\n", 1);
									break;
					}
					if(breakearlyflg == 1)
						break;
				} else {
					free(tmp);
					fatal_error(parms, "option \"-type\" expects only one char as an argument\n", 1);
				}
			} else {
				free(tmp);
				fatal_error(parms, "option \"-type\" expects one more argument\n", 1);
			}
		} else if(strcmp(parms[argvc], "-print") == 0) {
			prntflg = 1;
			printentry(full_file_name);
		} else if(strcmp(parms[argvc], "-ls") == 0) {
			prntflg = 1;
			printentry_ls(full_file_name, stats);
		} else {
			free(tmp);
			fprintf(stderr, "Invalid argument: %s\n", parms[argvc]);
			fatal_error(parms, "", 1);
		}
		argvc++;
	}

	if(prntflg == 0 && breakearlyflg == 0) {
		printentry(full_file_name);
	}

	if(dir_name != NULL) {
		free(dir_name);
	 } else if(file_name != NULL) {
		free(file_name);
	}

	free(tmp);
}

/**
 *
 * \brief Removes last slash from a string, if there is one.
 *
 * Checks if a string has at least one char and then removes da ending
 * '/' if there is one.
 *
 * \param char * String from which to remove ending /
 *
 * \return char *
 * \retval Any String without the ending /
 *
 */
char * strip_slashes(char * str) {
	while((strlen(str) > 0) && str[strlen(str)-1] == '/') {
		str[strlen(str)-1] = 0;
	}

	return str;
}

/**
 *
 * \brief Reads filetype converts to char and returns value.
 * 
 * Reads the filetype from the lstat struct and returns the resulting character.
 *
 * \param struct stat *
 *
 * \return char
 * \retval '?' something went wrong
 * \retval '-' file
 * \retval 'd' directory
 * \retval 'c' char special file
 * \retval 'b' block special file
 * \retval 'p' pipe
 * \retval 'l' link
 * \retval 's' socket
  */
char getentrytype(struct stat * stats) {
	char filemode = '?';
	if(S_ISREG(stats->st_mode)) filemode = '-';
	else if(S_ISDIR(stats->st_mode)) filemode = 'd';
	else if(S_ISCHR(stats->st_mode)) filemode = 'c';
	else if(S_ISBLK(stats->st_mode)) filemode = 'b';
	else if(S_ISFIFO(stats->st_mode)) filemode = 'p';
	else if(S_ISLNK(stats->st_mode)) filemode = 'l';
	else if(S_ISSOCK(stats->st_mode)) filemode = 's';

	return filemode;
}

/**
 *
 * \brief Checks if user name exists
 *
 * Checks if a certain username exists.
 * 
 * \param name String of the user name.
 * 
 * \return int
 * \retval 0 FALSE: group name does not exist. 
 * \retval 1 TRUE: group name does exit.
 *
 */
int user_exists(const char * const name) {
	if(getpwnam(name) == NULL)
		return 0;
	else
		return 1;
}

/**
 *
 * \brief Checks if group name exists
 *
 * Checks if a certain groupname exists. 
 * 
 * \param name String of the group name.
 * 
 * \return int
 * \retval 0 FALSE: group name does not exist. 
 * \retval 1 TRUE: group name does exit.
 *
 */
int group_exists(const char * const name) {
	if(getgrnam(name) == NULL)
		return 0;
	else
		return 1;
}

/**
 *
 * \brief Retrieves user-name or user-id and returns status.
 *
 * This function is used to store the user-name that suits to the given
 * uid into a given buffer. If a user-name does not exist it stores the
 * user-id itselfs as a string. The return values gives information
 * about the success of the given operation. (see below)
 *
 * \param gid UserID  
 * \param buf Buffer that will fill with user-name or id
 * \param size defines the max-size of pw_name, see MAX_PWNAME_SIZE
 *
 * \return enum getentryresult
 * \retval ID_NOT_FOUND
 * \retval ENTRY_ERROR
 * \retval ID_FOUND
 *
 */
enum getentryresult getentryowner(uid_t uid, char ** buf, size_t size) {
	struct passwd *pwd;
	pwd = getpwuid(uid);

	if(pwd == NULL) {
		if(errno == 0 || errno == ENOENT || errno == ESRCH || errno == EBADF || errno == EPERM) {
			snprintf(*buf, MAX_UGID_SIZE, "%i", (int) uid);
			return ID_NOT_FOUND;
		}
		else if(errno == EINTR) fprintf(stderr, "A signal was caught.\n");
		else if(errno == EIO) fprintf(stderr, "I/O error.\n");
		else if(errno == EMFILE) fprintf(stderr, "The maximum number (OPEN_MAX) of files was open already in the calling process.\n");
		else if(errno == ENFILE) fprintf(stderr, "The maximum number of files was open already in the system.\n");
		else if(errno == ENOMEM) fprintf(stderr, "Insufficient memory to allocate passwd structure.\n");
		else if(errno == ERANGE) fprintf(stderr, "Insufficient buffer space supplied.\n");
		else fprintf(stderr, "Unknown Error: %i.\n", errno);

		return ENTRY_ERROR;
	}

	strncpy(*buf, pwd->pw_name, size);
	return ID_FOUND;
}

/**
 *
 * \brief Retrieves group-name or group-id and return status.
 *
 * This function is used to store the group-name that suits to the given
 * uid into a given buffer. If a group-name does not exist it stores the
 * group-id itselfs as a string. The return values gives information
 * about the success of the given operation. (see below)
 *
 * \param gid GroupID  
 * \param buf Buffer that will fill with group-name or id
 * \param size defines the max-size of gr_name, see MAX_GRNAME_SIZE
 *
 * \return enum getentryresult
 * \retval ID_NOT_FOUND
 * \retval ENTRY_ERROR
 * \retval ID_FOUND
 *
 */
enum getentryresult getentrygroup(gid_t gid, char ** buf, size_t size) {
	struct group *grp;
	grp = getgrgid(gid);

	if(grp == NULL) {
		if(errno == 0 || errno == ENOENT || errno == ESRCH || errno == EBADF || errno == EPERM) {
			snprintf(*buf, MAX_UGID_SIZE, "%i", (int) gid);
			return ID_NOT_FOUND;
		}
		else if(errno == EINTR) fprintf(stderr, "A signal was caught.\n");
		else if(errno == EIO) fprintf(stderr, "I/O error.\n");
		else if(errno == EMFILE) fprintf(stderr, "The maximum number (OPEN_MAX) of files was open already in the calling process.\n");
		else if(errno == ENFILE) fprintf(stderr, "The maximum number of files was open already in the system.\n");
		else if(errno == ENOMEM) fprintf(stderr, "Insufficient memory to allocate passwd structure.\n");
		else if(errno == ERANGE) fprintf(stderr, "Insufficient buffer space supplied.\n");
		else fprintf(stderr, "Unknown Error: %i.\n", errno);

		return ENTRY_ERROR;
	}

	strncpy(*buf, grp->gr_name, size);
	return ID_FOUND;
}

/**
 *
 * \brief Output on stderr and exits program.
 *
 * Output of a passed string on stderr and exits program with exitcode.
 *
 * \param msg A String.
 * \param int Returnvalue for OS, which is passed on exit.
 * \param argv The argument vector.
 *
 * \return void.
 *
 */
void fatal_error(const char * const argv[], char * msg, int exitcode) {
	fprintf(stderr, "%s: %s", argv[0], msg);
	exit(exitcode);
}

/**
 *
 * \brief Checks if passed string is numeric.
 *
 * Checks if every char of a passed string is a digit.
 *
 * \param str A string.
 *
 * \return int 
 * \retval 0 FALSE: Is not a numeric string.
 * \retval 1 TRUE: Is a numeric string.
 *
 */
int is_numeric(char * str) {
	unsigned int i = 0;
	unsigned int len = strlen(str);

	while(i < len) {
		if(! isdigit(str[i]))
			return 0;
		i++;
	}

	return 1;
}

/**
 *
 * \brief Outputs file-information in simple format
 *
 * Uses the simple output format for the find query. (default)
 *  
 * \param full_file_name complete filename including path and file name, if present.
 *
 * \return void
 *
 */
void printentry(const char * full_file_name) {
	fprintf(stdout, "%s\n", full_file_name);
}

/**
 *
 * \brief Outputs file-information in longlist format
 *
 * Uses the longlist output format for the find query.
 * 
 * \param full_file_name complete filename including path and file name, if present.
 * \param stats lstat result struct.
 *
 * \return void
 *
 */
void printentry_ls(const char * full_file_name, struct stat stats) {
	char mode_readable[11] = {'?', '-', '-', '-', '-', '-', '-', '-', '-', '-', 0};
	char time_readable[13];
	unsigned long blocks;
	char month[4];
	char * s_uid = malloc(MAX_PWNAME_SIZE);
	char * s_gid = malloc(MAX_GRNAME_SIZE);
	struct tm * time;

	time = localtime(&stats.st_mtime);

	mode_readable[0] = getentrytype(&stats);

	if(stats.st_mode & S_IRUSR) mode_readable[1] = 'r';
	if(stats.st_mode & S_IWUSR) mode_readable[2] = 'w';
	if((stats.st_mode & S_IXUSR) && !(stats.st_mode & S_ISUID)) mode_readable[3] = 'x';
	else if((stats.st_mode & S_IXUSR) && (stats.st_mode & S_ISUID)) mode_readable[3] = 's';
	else if(!(stats.st_mode & S_IXUSR) && (stats.st_mode & S_ISUID)) mode_readable[3] = 'S';

	if(stats.st_mode & S_IRGRP) mode_readable[4] = 'r';
	if(stats.st_mode & S_IWGRP) mode_readable[5] = 'w';
	if((stats.st_mode & S_IXGRP) && !(stats.st_mode & S_ISGID)) mode_readable[6] = 'x';
	else if((stats.st_mode & S_IXGRP) && (stats.st_mode & S_ISGID)) mode_readable[6] = 's';
	else if(!(stats.st_mode & S_IXGRP) && (stats.st_mode & S_ISGID)) mode_readable[6] = 'S';

	if(stats.st_mode & S_IROTH) mode_readable[7] = 'r';
	if(stats.st_mode & S_IWOTH) mode_readable[8] = 'w';
	if((stats.st_mode & S_IXOTH) && !(stats.st_mode & S_ISVTX)) mode_readable[9] = 'x';
	else if((stats.st_mode & S_IXOTH) && (stats.st_mode & S_ISVTX)) mode_readable[9] = 't';
	else if(!(stats.st_mode & S_IXOTH) && (stats.st_mode & S_ISVTX)) mode_readable[9] = 'T';

	if(strftime(month, sizeof(month), "%b", time) == 0) {
		strcpy(time_readable, "????????????");
	} else {
		snprintf(time_readable, 13, "%s %2i %02i:%02i", month, time->tm_mday, time->tm_hour, time->tm_min);
	}

	blocks = (unsigned long) stats.st_blocks;
	if(getenv("POSIXLY_CORRECT") == NULL) {
		blocks /= 2;
	}

	getentryowner(stats.st_uid, &s_uid, MAX_PWNAME_SIZE);
	getentrygroup(stats.st_gid, &s_gid, MAX_GRNAME_SIZE);

	printf("%6lu %4lu %s %3i %-8s %-8s %8lu %s %s\n",
			(unsigned long) stats.st_ino,
			blocks,
			mode_readable,
			(int) stats.st_nlink,
			s_uid,
			s_gid,
			(unsigned long) stats.st_size,
			time_readable,
			full_file_name);

	free(s_uid);
	free(s_gid);
}

/**
 *
 * \brief Prints usage on param -h --help or if no param is offered.
 *
 * Prints usage-instruction for the program.
 *
 * \param argv arguments vector of program
 *
 * \return void
 *
 */
void printusage(char * argv[]) {
	printf("\nUsage: %s <path> [action]\n\n"\
		"valid actions:\n"\
		" -h | --help\n"\
		" -user <name>|<uid>\n"\
		" -nouser\n"\
		" -name <pattern>\n"\
		" -path <pattern>\n"\
		" -type [bcdpfls]\n"\
		" -print\n"\
		" -ls\n\n", argv[0]);
}
